/* gdbmopen.c - Open the dbm file and initialize data structures for use. */

/*  GNU DBM  - DataBase Manager (database subroutines) by Philip A. Nelson
    Copyright (C) 1989  Free Software Foundation, Inc.

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

    You may contact the author by:
       e-mail:  phil@wwu.edu
      us-mail:  Philip A. Nelson
                Computer Science Department
                Western Washington University
                Bellingham, WA 98226
        phone:  (206) 676-3035

*************************************************************************/

#include "gdbm.h"
#include "extern.h"
#include "gdbmerrno.h"
#include "gdbmutils.h"

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "kernel.h"
#include "swis.h"
#include "utils.h"

/* Initialize dbm system.  FILE is a pointer to the file name.  If the file
   has a size of zero bytes, a file initialization procedure is performed,
   setting up the initial structure in the file.  BLOCK_SIZE is used during
   initialization to determine the size of various constructs.  If the value
   is less than 512, the value 512 is used, otherwise the value of BLOCK_SIZE
   is used.  BLOCK_SIZE is ignored if the file has previously initialized.
   If READ_WRITE is set to DBM_READ the user wants to just read the database
   and any call to dbm_store or dbm_delete will fail. Many readers can access
   the database at the same time.  If READ_WRITE is set to DBM_WRITE, the user
   wants both read and write access to the database and requires exclusive
   access.  Any error detected will cause a return value of null and an
   approprate value will be in gdbm_errno.  If no errors occur, a pointer
   to the "dbm file descriptor" will be returned. */


dbm_file_info *gdbm_open(char *file, int block_size, int read_write, void (*fatal_func) ())
{
	dbm_file_info *dbf;	/* The record to return. */
	int exists;		/* File type (none, normal, dir). */
	int len;		/* Length of the file name. */
	int num_bytes;		/* Used in reading and writing. */
	_kernel_osfile_block blk;	/* Used to get file length &
					 * existence */

	/* Allocate new info structure. */
	dbf = (dbm_file_info *) malloc(sizeof(dbm_file_info));
	if (dbf == NULL)
	{
		gdbm_errno = MALLOC_ERROR;
		return NULL;
	}

	/*
	 * Initialize some fields for known values.  This is done so dbmclose
	 * will work if called before allocating some structures.
	 */
	dbf->dir = NULL;
	dbf->bucket = NULL;
	dbf->avail = NULL;

	/* Save name of file. */
	len = strlen(file);
	dbf->name = (char *) malloc(len + 1);
	if (dbf->name == NULL)
	{
		free(dbf);
		gdbm_errno = MALLOC_ERROR;
		return NULL;
	}
	strcpy(dbf->name, file);

	/* Initialize the fatal error routine. */
	dbf->fatal_err = fatal_func;

	/* Open the file. */
	if (read_write == DBM_READER)
	{
		exists = _kernel_osfile(5, dbf->name, &blk);
		if (exists != 1 || (dbf->desc = fopen(dbf->name, "r")) == NULL)
		{
			free(dbf->name);
			free(dbf);
			gdbm_errno = FILE_OPEN_ERROR;
			return NULL;
		}
	}
	else
	{
		exists = _kernel_osfile(17, dbf->name, &blk);
		if (exists == 0)
			dbf->desc = fopen(dbf->name, "w+");
		else
			dbf->desc = fopen(dbf->name, "r+");

		if (exists == 2 || dbf->desc == NULL)
		{
			free(dbf->name);
			free(dbf);
			gdbm_errno = FILE_OPEN_ERROR;
			return NULL;
		}
	}

	/* Check that the file is not empty (if reading). */
	if (read_write == DBM_READER)
	{
		if (blk.start == 0)
		{
			fclose(dbf->desc);
			free(dbf->name);
			free(dbf);
			gdbm_errno = EMPTY_DATABASE;
			return NULL;
		}
	}

#ifdef RANDOM_HASH
	/* Set up the random number generator used by the hash function. */
	setstate(initstate(567943, _dbm_hash_state, 64));
#endif				/* RANDOM_HASH */

	/* Record the kind of user. */
	dbf->read_write = read_write;

	/* Decide if this is a new file or an old file. */
	if (exists == 0 || blk.start == 0)
	{
		int index;

		/* This is a new file.  Create an empty database.  */

		/* Start with the blocksize. */
		if (block_size < 512)
			dbf->header.block_size = 512;
		else
			dbf->header.block_size = block_size;

		/* Create the initial hash table directory.  */
		dbf->header.dir_size = 8 * sizeof(int);
		dbf->header.dir_bits = 3;
		while (dbf->header.dir_size < dbf->header.block_size / 2)
		{
			dbf->header.dir_size <<= 1;
			dbf->header.dir_bits += 1;
		}
		dbf->dir = (int *) malloc(dbf->header.dir_size);
		if (dbf->dir == NULL)
		{
			gdbm_close(dbf);
			gdbm_errno = MALLOC_ERROR;
			return NULL;
		}
		dbf->header.dir = sizeof(dbf->header);

		/* Create the first and only hash bucket. */
		dbf->header.bucket_elems = (dbf->header.block_size - 2 * sizeof(int))
			/ sizeof(bucket_element);
		dbf->header.bucket_size = dbf->header.bucket_elems * sizeof(bucket_element)
			+ 2 * sizeof(int);
		dbf->bucket_adr = dbf->header.block_size;
		dbf->bucket_dir = 0;
		dbf->bucket = (hash_bucket *) (malloc(dbf->header.bucket_size));
		if (dbf->bucket == NULL)
		{
			gdbm_close(dbf);
			gdbm_errno = MALLOC_ERROR;
			return NULL;
		}
		_dbm_new_bucket(dbf, dbf->bucket, 0);

		/* Set table entries to point to hash buckets. */
		for (index = 0; index < dbf->header.dir_size / sizeof(int); index++)
			dbf->dir[index] = dbf->bucket_adr;

		/* Make the initial avail block. */
		dbf->avail = (avail_block *) malloc(dbf->header.block_size);
		if (dbf->avail == NULL)
		{
			gdbm_close(dbf);
			gdbm_errno = MALLOC_ERROR;
			return NULL;
		}
		dbf->avail_adr = 2 * dbf->header.block_size;
		for (index = 0; index < AVAIL_SIZE; index++)
			dbf->header.avail_list[index] = dbf->avail_adr;
		dbf->avail->size = ((dbf->header.block_size - sizeof(avail_block))
				    / sizeof(avail_elem)) + 1;
		dbf->avail->count = 0;
		dbf->avail->next_block = 0;
		dbf->avail->prev_block = 0;


		/* Other fields in the file header. */
		dbf->header.file_size = 3 * dbf->header.block_size;
		dbf->header.header_magic = 0x13579ace;
		/* Set this field so we do crash detection and recovery. */
		dbf->header.update_state = 0;

		/* Write initial configuration to the file. */
		num_bytes = fwrite(&dbf->header, 1, sizeof(dbf->header), dbf->desc);
		if (num_bytes != sizeof(dbf->header))
		{
			gdbm_close(dbf);
			gdbm_errno = FILE_WRITE_ERROR;
			return NULL;
		}

		num_bytes = fwrite(dbf->dir, 1, dbf->header.dir_size, dbf->desc);
		if (num_bytes != dbf->header.dir_size)
		{
			gdbm_close(dbf);
			gdbm_errno = FILE_WRITE_ERROR;
			return NULL;
		}

		num_bytes = fseek(dbf->desc, dbf->header.block_size, SEEK_SET);
		if (num_bytes != 0)
		{
			gdbm_close(dbf);
			gdbm_errno = FILE_SEEK_ERROR;
			return NULL;
		}
		num_bytes = fwrite(dbf->bucket, 1, dbf->header.bucket_size, dbf->desc);
		if (num_bytes != dbf->header.bucket_size)
		{
			gdbm_close(dbf);
			gdbm_errno = FILE_WRITE_ERROR;
			return NULL;
		}

		num_bytes = fseek(dbf->desc, 2L * dbf->header.block_size, SEEK_SET);
		if (num_bytes != 0)
		{
			gdbm_close(dbf);
			gdbm_errno = FILE_SEEK_ERROR;
			return NULL;
		}
		num_bytes = fwrite(dbf->avail, 1, dbf->header.block_size, dbf->desc);
		if (num_bytes != dbf->header.block_size)
		{
			gdbm_close(dbf);
			gdbm_errno = FILE_WRITE_ERROR;
			return NULL;
		}

		fflush(dbf->desc);

		/* Finally, "free" the unused space in the initial file. */
		index = sizeof(dbf->header) + dbf->header.dir_size;
		_dbm_free(dbf, index, dbf->header.block_size - index);
		_dbm_free(dbf, dbf->bucket_adr + dbf->header.bucket_size,
			  dbf->header.block_size - dbf->header.bucket_size);
		_dbm_end_update(dbf);

	}
	else
	{
		/*
		 * This is an old database.  Read in the information from the
		 * file header and initialize the hash directory.
		 */

		/* Read the file header. */
		num_bytes = fread(&dbf->header, 1, sizeof(dbf->header), dbf->desc);
		if (num_bytes != sizeof(dbf->header))
		{
			gdbm_close(dbf);
			gdbm_errno = FILE_READ_ERROR;
			return NULL;
		}

		/* Is the magic number good? */
		if (dbf->header.header_magic != 0x13579ace)
		{
			gdbm_close(dbf);
			gdbm_errno = BAD_MAGIC_NUMBER;
			return NULL;
		}

		/* Is the database consistent? */
		if (dbf->header.update_state != 0 && dbf->header.update_state != -1)

			/*
			 * If it is not in the middle of an update or we are
			 * a reader we can not continue.
			 */
			if ((dbf->header.update_state & 0xffffff00) != 0x77777700
			    || read_write == DBM_READER)
			{
				gdbm_close(dbf);
				gdbm_errno = READER_CANT_RECOVER;
				return NULL;
			}

		/* Allocate space for the hash table directory.  */
		dbf->dir = (int *) malloc(dbf->header.dir_size);
		if (dbf->dir == NULL)
		{
			gdbm_close(dbf);
			gdbm_errno = MALLOC_ERROR;
			return NULL;
		}

		/* Read the hash table directory. */
		num_bytes = fseek(dbf->desc, dbf->header.dir, SEEK_SET);
		if (num_bytes != 0)
		{
			gdbm_close(dbf);
			gdbm_errno = FILE_SEEK_ERROR;
			return NULL;
		}

		num_bytes = fread(dbf->dir, 1, dbf->header.dir_size, dbf->desc);
		if (num_bytes != dbf->header.dir_size)
		{
			gdbm_close(dbf);
			gdbm_errno = FILE_READ_ERROR;
			return NULL;
		}

		/* Initialize the hash bucket buffer.  */
		dbf->bucket = (hash_bucket *) malloc(dbf->header.bucket_size);
		if (dbf->bucket == NULL)
		{
			gdbm_close(dbf);
			gdbm_errno = MALLOC_ERROR;
			return NULL;
		}
		dbf->bucket_adr = 0;

		/* Initialize the avail block buffer.  */
		dbf->avail = (avail_block *) malloc(dbf->header.block_size);
		if (dbf->avail == NULL)
		{
			gdbm_close(dbf);
			gdbm_errno = MALLOC_ERROR;
			return NULL;
		}
		dbf->avail_adr = 0;

		/*
		 * Now that everything has been read and initialized, if we
		 * were in an update, it is now time to fix the database.
		 */
		if ((dbf->header.update_state & 0xffffff00) == 0x77777700)
		{
			int update_code;
			int param1;
			int param2;
			int param3;
			int param4;

			/*
			 * Crash recovery!  Save the information from the
			 * header in local variables so that the information
			 * is not lost.
			 */

			update_code = dbf->header.update_state & 0xff;
			param1 = dbf->header.upd_param1;
			param2 = dbf->header.upd_param2;
			param3 = dbf->header.upd_param3;
			param4 = dbf->header.upd_param4;
			switch (update_code)
			{
			case 1:/* _file_alloc - updating current avail
				 * block. param1 = address of new allocation.
				 * param2 = size of new allocation. param3 =
				 * size of unused portion. param4 = address
				 * of avail block. */

			case 2:/* _file_alloc - allocating at the end of the
				 * file. param1 = address of new allocation.
				 * param2 = size of new allocation. param3 =
				 * address of unused portion of previous
				 * block. param4 = size of unused portion.  */


			case 3:/* _dbm_free - updating current avail block.
				 * param1 = address of new available block.
				 * param2 = size of new avaliable block.
				 * param3 = address of the avail block.
				 * param4 = size of avail block.  */


			case 4:/* _dbm_free - splitting current avail block.
				 * Sharing with the previous block. param1 =
				 * address of current block. param2 = size of
				 * current block before split. param3 =
				 * address of previous block. param4 = size
				 * of previous block before split. */

			case 5:/* _dbm_free - splitting current avail block.
				 * Sharing with the next block. param1 =
				 * address of current block. param2 = size of
				 * current block before split. param3 =
				 * address of next param4 = size of unused
				 * portion before split.  */

			case 6:/* _dbm_free - splitting current avail block.
				 * Creating a new block. param1 = address of
				 * current block. param2 = size of current
				 * block before the split. param3 = address
				 * of new block. param4 = address of the next
				 * block.  */

				/*
				 * Cases 1 to 6 call a repair routine in
				 * falloc.c.
				 */

				_dbm_alloc_repair(dbf, update_code, param1, param2, param3, param4);
				break;

			case 7:/* gdbm_store - writing the data to the file.
				 * param1 = data pointer param2 = total size
				 * (key and data) param3 = current bucket
				 * address param4 = element location in the
				 * current bucket */

				/* Read the bucket to figure out what to do. */
				dbf->bucket_adr = param3;
				num_bytes = fseek(dbf->desc, dbf->bucket_adr, SEEK_SET);
				if (num_bytes != 0)
				{
					gdbm_close(dbf);
					gdbm_errno = FILE_SEEK_ERROR;
					return NULL;
				};
				num_bytes = fread(dbf->bucket, 1, dbf->header.bucket_size, dbf->desc);
				if (num_bytes != dbf->header.bucket_size)
				{
					gdbm_close(dbf);
					gdbm_errno = FILE_READ_ERROR;
					return NULL;
				};

				/*
				 * Check to see if bucket was updated. -1 =>
				 * no update wrong address => no update In
				 * both cases, we don't need to touch the
				 * bucket, we just free the space allocated
				 * for the new item. If it was updated, we
				 * don't need to do anything.
				 */

				if (dbf->bucket->h_table[param4].hash_value == -1
				    || dbf->bucket->h_table[param4].data_pointer != param1)
				{
					/*
					 * We need to free the allocated
					 * space.
					 */
					_dbm_free(dbf, param1, param2);
				}

				break;

			case 8:/* split_current - splitting a bucket. param1
				 * = new bucket address param2 = old bucket
				 * size param3 = old bucket address param4 =
				 * old bucket directory entry  */

				/* Read the bucket to figure out what to do. */
				dbf->bucket_adr = param3;
				num_bytes = fseek(dbf->desc, dbf->bucket_adr, SEEK_SET);
				if (num_bytes != 0)
				{
					gdbm_close(dbf);
					gdbm_errno = FILE_SEEK_ERROR;
					return NULL;
				};
				num_bytes = fread(dbf->bucket, 1, dbf->header.bucket_size, dbf->desc);
				if (num_bytes != dbf->header.bucket_size)
				{
					gdbm_close(dbf);
					gdbm_errno = FILE_READ_ERROR;
					return NULL;
				};

				if (dbf->bucket->count == param3)
				{
					/*
					 * The old bucket was not saved.
					 * Toss the new bucket!
					 */
					_dbm_free(dbf, param1, dbf->header.bucket_size);
				}
				else
				{
					/*
					 * The old bucket was saved.  Make
					 * sure the table is correct.
					 */
					int dir_start, dir_end, index, new_bits;

					new_bits = dbf->bucket->bucket_bits;
					dir_start = (param4 >> (dbf->header.dir_bits - new_bits)) | 1;
					dir_end = (dir_start + 1) << (dbf->header.dir_bits - new_bits);
					dir_start = dir_start << (dbf->header.dir_bits - new_bits);
					if (dbf->dir[dir_start] != param1)
					{
						for (index = dir_start; index < dir_end; index++)
							dbf->dir[index] = param1;
						num_bytes = fseek(dbf->desc, dbf->header.dir, SEEK_SET);
						if (num_bytes != 0)
							_dbm_fatal(dbf, "seek error");
						num_bytes = fwrite(dbf->dir, 1, dbf->header.dir_size,
								 dbf->desc);
						if (num_bytes != dbf->header.dir_size)
							_dbm_fatal(dbf, "write error");
					}

				}

				break;

			case 9:/* gdbm_delete - updating the current bucket.
				 * param1 = data pointer param2 = total size
				 * (key and data) param3 = bucket address
				 * param4 = bucket count after delete */

				/* Read the bucket to figure out what to do. */
				dbf->bucket_adr = param3;
				num_bytes = fseek(dbf->desc, dbf->bucket_adr, SEEK_SET);
				if (num_bytes != 0)
				{
					gdbm_close(dbf);
					gdbm_errno = FILE_SEEK_ERROR;
					return NULL;
				};
				num_bytes = fread(dbf->bucket, 1, dbf->header.bucket_size, dbf->desc);
				if (num_bytes != dbf->header.bucket_size)
				{
					gdbm_close(dbf);
					gdbm_errno = FILE_READ_ERROR;
					return NULL;
				};
				if (dbf->bucket->count == param4)
				{
					/*
					 * It was written out, just free the
					 * space.
					 */
					_dbm_free(dbf, param1, param2);
				}
				else
				{
					/*
					 * It was not written out, must
					 * delete the element.
					 */
					datum key;
					int index, num_bytes;

					index = 0;
					while (dbf->bucket->h_table[index].data_pointer != param3)
						index++;

					/* Delete the index item. */
					key.dsize = dbf->bucket->h_table[index].key_size;
					key.dptr = (char *) malloc(key.dsize);
					num_bytes = fseek(dbf->desc, param3, SEEK_SET);
					if (num_bytes != param3)
					{
						free(key.dptr);
						gdbm_close(dbf);
						gdbm_errno = FILE_SEEK_ERROR;
						return NULL;
					};
					num_bytes = fread(key.dptr, 1, key.dsize, dbf->desc);
					if (num_bytes != key.dsize)
					{
						free(key.dptr);
						gdbm_close(dbf);
						gdbm_errno = FILE_READ_ERROR;
						return NULL;
					};
					gdbm_delete(dbf, key);
					free(key.dptr);
				}
				break;

			default:
				/* Unknown update code! */
				gdbm_close(dbf);
				gdbm_errno = UNKNOWN_UPDATE;
				return NULL;
			}

			/* The correction was completed correctly. */
			_dbm_end_update(dbf);
		}
	}

	/*
	 * Everything is fine, return the pointer to the file information
	 * structure.
	 */
	return dbf;

}
